import hashlib
from node import Node
import optparse
import os
import pprint
import properties
import re
from revision import Revision
import StringIO
import sys

DarkYellow = "\033[33m"
LightYellow = "\033[33;1m"
DarkGreen = "\033[32m"
LightGreen = "\033[32;1m"
Gray = "\033[30;1m"
LightRed = "\033[31;1m"
DarkRed = "\033[31m"
White = "\033[0m"
DarkBlue = "\033[34m"
LightBlue = "\033[34;1m"

parser = optparse.OptionParser()
parser.add_option("", "--in", dest = "in_file", help = "The svn dump file to process.")
parser.add_option("", "--out", dest = "out_file", help = "The new svn dump file to write")
parser.add_option("", "--overwrite-output-file", action = "store_true", default = False, dest = "overwrite_output_file", help = "Allow the output file to be overwritten.")
parser.add_option("", "--path-filter", dest = "path_filter", default = ".*", help = "Regular expression for paths that will survive.")
parser.add_option("", "--rewrite-authors", dest = "rewrite_authors", help = "old_name|new_name,...")
parser.add_option("", "--set-author-in-revision", dest = "set_author_in_revision", help = "Specify a comma-sperarated list of revision:author that will be set.")
parser.add_option("", "--drop-revisions", dest = "drop_revisions", help = "Specify revision numbers to drop (e.g. n1,n2,n3)")
parser.add_option("", "--drop-paths", dest = "drop_paths", help = "Specify a list of paths to drop.")
parser.add_option("", "--drop-empty-revisions", action = "store_true", default = False, dest = "drop_empty_revisions", help = "Whether to drop revisions without a node.")
parser.add_option("", "--rewrite-path-prefixes", dest = "rewrite_path_prefixes", help = "Specify a comma-separated list of path prefix rewrites.")
parser.add_option("", "--drop-path-in-revision", dest = "drop_path_in_revision", help = "Specify a comma-sperarated list of revision:path that will be dropped.")

drop_revisions = list()
drop_paths = list()
rewrite_path_prefixes = list()
rewrite_authors = dict()
revision_mapping = dict()
next_revision_number = 0
drop_path_in_revision = dict()
set_author_in_revision = dict()

# read the options and validate
(options, args) = parser.parse_args()
if options.in_file == None:
	print("Set the input svn dump with '--in'.")
	exit(1)
if options.drop_revisions != None:
	drop_revisions = options.drop_revisions.split(",")
	drop_revisions = map(int, drop_revisions)
if options.drop_paths != None:
	drop_paths = options.drop_paths.split(",")
if options.rewrite_path_prefixes != None:
	for rewrite_path_prefix in options.rewrite_path_prefixes.split(","):
		rewrite_path_prefixes.append(rewrite_path_prefix.split("|"))
if options.rewrite_authors != None:
	for rewrite_author in options.rewrite_authors.split(","):
		old_name, new_name = rewrite_author.split("|")
		rewrite_authors[old_name] = new_name
if options.drop_path_in_revision != None:
	specifications = options.drop_path_in_revision.split(",")
	for specification in specifications:
		drop_path_in_revision[specification] = True
if options.set_author_in_revision != None:
	specifications = options.set_author_in_revision.split(",")
	for specification in specifications:
		revision, author = specification.split(":")
		set_author_in_revision[int(revision)] = author
drop_empty_revisions = options.drop_empty_revisions
path_filter = options.path_filter

# open the files
try:
	in_file = open(options.in_file, "r")
except IOError, exception:
	print("The input file " + LightYellow + options.in_file + White + " does not exist.")
	exit(1)
if options.out_file != None:
	try:
		if os.path.samefile(options.in_file, options.out_file) == True:
			print("The input file " + LightYellow + options.in_file + White + " and the output file " + LightYellow + options.out_file + White + " can not be the same.")
			exit(1)
	except OSError, exception:
		pass
	if os.path.exists(options.out_file) == True and options.overwrite_output_file == False:
		print("The output file " + LightYellow + options.out_file + White + " shall not exist to prevent overwriting important data.")
		print("If you know what you're doing use the option " + LightYellow + "--overwrite-output-file" + White + ".")
		exit(1)
	try:
		out_file = open(options.out_file, "w")
	except IOError, exception:
		print("The output file " + LightYellow + options.out_file + White + " could not be opened for writing.")
		exit(1)
else:
	out_file = None

# read the file header
caption, value = in_file.readline().strip().split(": ")
assert caption == "SVN-fs-dump-format-version"
if value != "2":
	print "This script only recognizes version 2 dumps."
	sys.exit(1)
assert in_file.readline() == "\n"
# write the header to out_file
if out_file != None:
	out_file.write("SVN-fs-dump-format-version: 2\n")
	out_file.write("\n")

# try to read UUID
caption, value = in_file.readline().split(": ")
if caption == "UUID":
	assert in_file.readline() == "\n"
	if out_file != None:
		out_file.write(line)
		out_file.write("\n")
	# now read the first revision line
	caption, value = in_file.readline().split(": ")
else:
	# we already got the first revision line
	pass

end_of_file = False
# this while loops over the revisions of the svn dump file
while end_of_file == False:
	assert caption == "Revision-number"
	last_node = None
	# after this we gather the properties of the revision
	revision = Revision()
	original_revision_number = revision.number = int(value.strip())
	properties_content_length = 0
	content_length = 0
	# this while loops over the properties of a revision until an empty line is reached
	while True:
		line = in_file.readline()
		if line == "\n":
			break
		caption, value = line.split(": ")
		value = value.strip()
		if caption == "Prop-content-length":
			properties_content_length = int(value)
		elif caption == "Content-length":
			content_length = int(value)
	assert properties_content_length == content_length
	# now read the data associated with the revision if available
	if properties_content_length > 0:
		revision.properties.parse(in_file.read(properties_content_length))
		assert in_file.readline() == "\n"
	# this while loops over the nodes and breaks upon reaching another revision
	while True:
		in_line = in_file.readline()
		if in_line == "":
			end_of_file = True
			break
		if in_line != "\n":
			caption, value = in_line.split(": ")
			if caption == "Revision-number":
				break
			assert caption == "Node-path"
			last_node = node = Node()
			# after this we gather the properties of the node
			node.path = value.strip()
			properties_content_length = None
			text_content_length = None
			text_content_md5 = None
			content_length = None
			# this while loops over the properties of a node until an empty line is reached
			while True:
				line = in_file.readline()
				if line == "\n":
					break
				caption, value = line.split(": ")
				value = value.strip()
				if caption == "Prop-content-length":
					properties_content_length = int(value)
				elif caption == "Text-content-length":
					text_content_length = int(value)
				elif caption == "Text-content-md5":
					text_content_md5 = value
				elif caption == "Node-action":
					node.action = value
				elif caption == "Node-kind":
					node.kind = value
				elif caption == "Node-copyfrom-path":
					node.copy_from_path = value
				elif caption == "Node-copyfrom-rev":
					node.copy_from_rev = int(value)
				elif caption == "Content-length":
					content_length = int(value)
			# now read the data associated with the node if available
			if properties_content_length != None:
				node.properties.parse(in_file.read(properties_content_length))
				node.force_properties = True
			if text_content_length != None:
				node.text = in_file.read(text_content_length)
			revision.nodes.append(node)
		else:
			last_node.trailing_newlines += 1
	
	# now apply any changes
	drop_revision = False
	# if the revision is requested to be dropped ...
	if revision.number in drop_revisions:
		# print information
		print DarkYellow + "Original revision " + str(original_revision_number) + White + " has been dropped."
		drop_revision = True
		# if the current revision is not existent, everything that referenced it in the old dump will have to refer to the previous revision in the new dump
		revision_mapping[original_revision_number] = revision_mapping[original_revision_number - 1]
	else:
		# revision is not meant to be dropped explicitely
		# assign the next revision number in the new sequence
		revision.number = next_revision_number
		# now filter those nodes that will survive
		node_index = 0
		while node_index < len(revision.nodes):
			if re.search(path_filter, revision.nodes[node_index].path) == None:
				del revision.nodes[node_index]
			else:
				node_index += 1
		# now exclude any nodes that are explicitely dropped by path
		node_index = 0
		while node_index < len(revision.nodes):
			deleted = False
			for drop_path in drop_paths:
				if revision.nodes[node_index].path.startswith(drop_path) == True:
					del revision.nodes[node_index]
					deleted = True
					break
			if deleted == False:
				node_index += 1
		# now drop any nodes that are dropped by revision and path
		node_index = 0
		while node_index < len(revision.nodes):
			if str(original_revision_number) + ":" + revision.nodes[node_index].path in drop_path_in_revision:
				del revision.nodes[node_index]
			else:
				node_index += 1
		# now rewrite the path prefixes of remaining nodes
		for node in revision.nodes:
			for old_path_prefix, new_path_prefix in rewrite_path_prefixes:
				if node.path.startswith(old_path_prefix) == True:
					node.path = new_path_prefix + node.path[len(old_path_prefix):]
					break
		# now rename the copy-sources
		for node in revision.nodes:
			if node.copy_from_path != None:
				for old_path_prefix, new_path_prefix in rewrite_path_prefixes:
					if node.copy_from_path.startswith(old_path_prefix) == True:
						node.copy_from_path = new_path_prefix + node.copy_from_path[len(old_path_prefix):]
						break
		# delete this revision if it doesn't contain nodes and empty revisions are to be dropped
		if len(revision.nodes) == 0 and drop_empty_revisions == True and original_revision_number != 0:
			drop_revision = True
			# if the current revision is not existent, everything that referenced it in the old dump will have to refer to the previous revision in the new dump
			revision_mapping[original_revision_number] = revision_mapping[original_revision_number - 1]
		else:
			# otherwise remap the copy_revs in the nodes where applicable
			for node in revision.nodes:
				if node.copy_from_rev != None:
					assert revision_mapping[node.copy_from_rev] != None
					node.copy_from_rev = revision_mapping[node.copy_from_rev]
			# map the old revision number to the number in this sequence
			revision_mapping[original_revision_number] = revision.number
			# rewrite the author if applicable
			if original_revision_number in set_author_in_revision:
				revision.properties["svn:author"] = set_author_in_revision[original_revision_number]
			else:
				if "svn:author" in revision.properties and revision.properties["svn:author"] in rewrite_authors:
					revision.properties["svn:author"] = rewrite_authors[revision.properties["svn:author"]]
	
	# output of the new values if it is still in the game
	if drop_revision == True:
		print DarkYellow + "Original revision " + str(original_revision_number) + White + " has been dropped."
	else:
		next_revision_number += 1
		message = "Original revision " + DarkYellow + str(original_revision_number) + White + " => " + LightYellow + str(revision.number) + White
		if "svn:author" in revision.properties:
			message += " committed by " + LightGreen + revision.properties["svn:author"] + White
		if "svn:date" in revision.properties:
			message += " on " + DarkGreen + revision.properties["svn:date"] + White
		print message
		if "svn:log" in revision.properties:
			if revision.properties["svn:log"] == "":
				print "\tLog: \"svn:log\" property exists but is empty."
			else:
				print "\tLog: " + Gray + revision.properties["svn:log"].replace("\n", "\n\t     ") + White
		for node in revision.nodes:
			# now write to standard output
			message = "\t"
			if node.kind == "dir":
				message += "Directory: "
				if node.action == "add":
					if node.copy_from_path != None:
						message += DarkBlue + "+"
					else:
						message += DarkGreen + "+"
				elif node.action == "change":
					message += LightYellow + "~"
				elif node.action == "delete":
					message += LightRed + "-"
				else:
					assert False
			elif node.kind == "file":
				message += "File: "
				if node.action == "add":
					if node.copy_from_path != None:
						message += DarkBlue + "+"
					else:
						message += DarkGreen + "+"
				elif node.action == "change":
					message += LightYellow + "~"
				elif node.action == "delete":
					message += LightRed + "-"
				else:
					assert False
			else:
				message += "Node: "
				if node.action == "delete":
					message += LightRed + "-"
				else:
					assert False
			message += White + " " + LightBlue + node.path + White
			if node.copy_from_path != None or node.copy_from_rev != None:
				assert node.copy_from_rev != None and node.copy_from_rev != None
				message += " from " + DarkBlue + node.copy_from_path + White + "[" + DarkYellow + str(node.copy_from_rev) + White + "]"
			print message
		# now write the revision data to out_file
		if out_file != None:
			out_file.write("Revision-number: " + str(revision.number) + "\n")
			out_file.write("Prop-content-length: " + str(revision.properties.get_length()) + "\n")
			out_file.write("Content-length: " + str(revision.properties.get_length()) + "\n")
			out_file.write("\n")
			out_file.write(revision.properties.get_string())
			out_file.write("\n")
			for node in revision.nodes:
				assert node.path != None
				out_file.write("Node-path: " + node.path + "\n")
				if node.kind != None:
					out_file.write("Node-kind: " + node.kind + "\n")
				assert node.action != None
				out_file.write("Node-action: " + node.action + "\n")
				if node.copy_from_rev != None:
					out_file.write("Node-copyfrom-rev: " + str(node.copy_from_rev) + "\n")
				if node.copy_from_path != None:
					out_file.write("Node-copyfrom-path: " + str(node.copy_from_path) + "\n")
				if node.properties.count > 0 or node.force_properties == True:
					out_file.write("Prop-content-length: " + str(node.properties.get_length()) + "\n")
				if node.text != None:
					out_file.write("Text-content-length: " + str(len(node.text)) + "\n")
				if node.text != None:
					out_file.write("Text-content-md5: " + node.text_md5 + "\n")
				if node.content_length > 0:
					out_file.write("Content-length: " + str(node.content_length) + "\n")
				out_file.write("\n")
				if node.properties.count > 0 or node.force_properties == True:
					out_file.write(node.properties.get_string())
				if node.text != None:
					out_file.write(node.text)
				for i in range(node.trailing_newlines):
					out_file.write("\n")
